Chapter 1. Algorithms With Numbers

注:在本章中,基于数本身的复杂度分析,基础数学运算不能视为$O(1)$,需要考虑比特长度。
下文中提到的 $N$ 一般指原数, $n$ 一般指 $N$ 的比特长度,即 $\log N$。

基础运算

加法运算:
我们先考虑两数之和的位数:至多 $n+1$ 位。(作业中证了)
我们考虑最简单的加法操作:一位一位相加进位,则复杂度为 $O(n)$ 。
那么有无更快算法呢?光是两数的读入都是 $O(n)$ 级别的。所以这种就是(不考虑常数)最快的了。

乘法运算:
首先考虑竖式类的计算:显然是 $O(n^2)$ 的。
我们还给出了一种乘法计算方式:

MULTIPLY(x, y)
Require: Two $n$-bit integers $x$ and $y$, where $y \geq 0$
Ensure: $x \cdot y$
if $y = 0$ then
return $0$
end if
$z \gets \text{MULTIPLY}(x, \lfloor y/2 \rfloor)$
if $y$ is even then
return $2z$
else
return $x + 2z$
end if

有 $n$ 次递归, 每次递归有一次复杂度为 $O(n)$ 的加法,故综合时间复杂度仍为 $O(n^2)$,不过理论上常数会小点。

有无更快的算法呢?有的兄弟有的。后面的FFT就能帮我们优化一部分。目前已有理论上 $O(n\log n)$ 的乘法算法了。
除法:
没有什么特殊的。我们给出一个伪代码:

DIVIDE($x$, $y$)
Require: Two $n$-bit integers $x$ and $y$, where $y \geq 1$
Ensure: Quotient $q$ and remainder $r$ such that $x = qy + r$ and $0 \le r < y$
if $x = 0$ then
return $(q, r) = (0, 0)$
end if
$(q, r) = \text{DIVIDE}(\lfloor x/2 \rfloor, y)$
$q = 2 \cdot q,\quad r = 2 \cdot r$
if $x$ is odd then
$r = r + 1$
end if
if $r \ge y$ then
$r = r - y,\quad q = q + 1$
end if
return $(q, r)$

时间复杂度为 $O(n^2)$ 。

取模运算

带模加法运算:
两个 $x, y$ 属于 $0\leq x,y < N$,所以最多一次减法,总体复杂度仍是 $O(n)$ 。

带模乘法运算:
考虑 $0 \leq x \cdot y \leq (N-1)^2$
结果的长度 $\log (N-1)^2 = 2\log(N-1)$,再考虑做一次除法获得余数,总体时间复杂度仍是 $O(n^2)$ 的。

带模除法运算:
首先要说明,带模除法并不总是合法的。只有在除数和模数互质,存在乘法逆元时,才是合法的。
可以在 $O(n^3)$ 时间内解决。

带模乘方运算:
快速幂。

欧几里得算法:
即辗转相除法算gcd。
给出伪代码:

Euclid($a$, $b$)
Require: Two integers $a$ and $b$ with $a \geq b \geq 0$
Ensure: $\gcd(a, b)$
if $b = 0$ then
return $a$
end if
return $\text{Euclid}(b, a \bmod b)$

值得注意的是,有Lemma

$$ a \geq b\geq 0, \text{则 }a \text{ mod } b \geq \frac{a}{2} $$

证明也很简单,如果 $b > \frac{a}{2}$,则有

$$ a \text{ mod } b = a - b < \frac{a}{2} $$

如果$b \leq \frac{a}{2}$,则有

$$ a \text{ mod } b < b \leq \frac{a}{2} $$

所以辗转相处的次数一定小于 $O(n)$ ,每次一次除法 $O(n^2)$,则得到 时间复杂度 $O(n^3)$ 。

扩展欧几里得:
主要用于解决这个式子:

$$ ax+by = d $$

同时我们有B´ezout’s lemma:
如果d是a,b的公因数,那么d只能是a,b的最大公因数,其他因数都不行。
证明很简单:
由d是a,b公因数,一定有:

$$ d \leq gcd(a, b) $$

同时,gcd(a, b)又一定整除 $ax+by$,所以:

$$ gcd(a, b) |d $$

即: $$gcd(a, b) \leq d$$
所以 d=gcd(a, b)。

然后我们继续看这个式子。
由于我们有:

$$ ax+by = gcd(a, b) $$

同理,又有:

$$ gcd(a,b)=gcd(b,a\text{ mod } b) $$

同时我们有 :

$$ a \text{ mod }b = a - b \cdot\left\lfloor \frac{a}{b} \right\rfloor $$

所以有

$$ \begin{align} bx \prime + \left( a - b \cdot\left\lfloor \frac{a}{b} \right\rfloor \right)y \prime = gcd(a, b) \\ ay\prime +b\left( x \prime - \left\lfloor \frac{a}{b} \right\rfloor y \prime \right) = gcd(a, b) \end{align} $$

所以我们就可以一路递归了。边界条件就是 $(1,0,a)$。
给出伪代码:

EXTENDED-EUCLID($a$, $b$)
Require: Two integers $a$ and $b$ with $a \geq b \geq 0$
Ensure: Integers $x, y, d$ such that $d = \gcd(a, b)$ and $ax + by = d$
if $b = 0$ then
return $(1, 0, a)$
end if
$(x', y', d) = \text{EXTENDED-EUCLID}(b, a \bmod b)$
return $(y',\; x' - \lfloor a/b \rfloor \cdot y',\; d)$

乘法逆元:
我们先阐明一下引入乘法逆元的原因。我们在前面说过,带模除法不总是成立的,主要是为了满足带模运算的那些性质。
比如说对模6我们除以2,就是乘以$\frac{1}{2}$。

$$ \begin{align} 6 \cdot \frac{1}{2} \equiv 3 \pmod{6} \\ 0 \cdot \frac{1}{2} \equiv 0 \pmod{6} \\ 6 \equiv 0 \pmod{6} \end{align} $$

不满足模运算的性质。
所以我们引入乘法逆元,即模意义下的倒数。
我们称 $x$ 是 $a$ mod $N$ 意义下的乘法逆元,当

$$ ax \equiv 1 \pmod{N} $$

记 $x = a ^{-1}$

同时我们有定理:
$a$在模$N$意义下存在乘法逆元当且仅当 gcd(a, N) = 1

证明一下:
存在逆元 $\implies$ gcd(a, N) = 1:
存在x使得 $ax \equiv 1 \pmod{N}$,则存在整数k使得

$$ ax + Nk = 1 $$

那么显然1是a和N的因数,由B´ezout’s lemma知 gcd(a,N) = 1

gcd(a, N) = 1 $\implies$ 存在逆元:

由B´ezout’s lemma知:

$$ ax + Nk = 1 $$

两边同时对N取模就得到x了。

那我们也看到这个证明的时候用了这么多B´ezout’s lemma,那我们直接对a,N用扩展欧几里得就直接算出来x。

素性检验:

我们来到伟大的费马小定理:
如果p是质数

$$ \forall 1\leq a< p ,a^{p-1} \equiv 1 \pmod{p} $$

一个简单的证明:
令 $S = \{1, 2\dots p-1\}$
我们首先claim一个事情:我们把对这个集合中的每个元素同时乘以a再对p取模,得到的新集合只是S的一个重排列。证明也是非常的简单啊,如果这个集合里有两个元素 i, j 乘以a后对p同余,那么有

$$ ai \equiv aj \pmod{p} $$

所以

$$ p|a(j-i) $$

其中有

$$ a 又有p是质数,我们知道这个式子可能成立。所以这个claim是没有问题的。
所以

$$ \begin{align} 1 \times 2\times\dots \cdot p-1 &\equiv (1\times a) \times (2\times a) \times \dots ((p-1)\times a) &\pmod{p} \\ (p-1)! &\equiv (p-1)! \times a^{p-1} &\pmod{p} \end{align} $$

两边一除就得到了

$$ a^{p-1} \equiv 1 \pmod{p} $$

☝我们想用这个性质来判断一个数是不是质数,有没有搞头。
所以我们给出下面的伪代码

PRIMALITY($N$)
Require: Positive integer $N$
Ensure: \textit{yes} (possibly prime) or \textit{no} (composite)
Pick a positive integer $a < N$ at random
if $a^{N-1} \equiv 1 \pmod{N}$ then
return \textit{yes}
else
return \textit{no}
end if

看起来很有道理,但实际上是有问题的。问题出在费马小定理不是充分必要的。
比如 : $341 = 11 \times 31$,但是 $2^{340} \equiv 1 \pmod{341}$,是不满足的。
所以我们希望:对于一个合数,绝大部分的a还是会满足这个条件,也就是反例没有很多。
很可惜,存在一类数比较有个性。Carmichael numbers。
所有与N互质的a都满足

$$ a^{N-1} \equiv 1 \pmod{N} $$

$561=3\times 11 \times 17$就是一个典型的Carmichael number。

我们先不管Carmichael number,先对non-Carmichael number研究。
我们先引入一个Lemma:
如果一个与N互质的a满足

$$ a^{N-1} \not\equiv 1 \pmod{N} $$

则小于N的a(互质数)里至少有一半a满足上面这条。
简短的证明:
如果存在

$$ b^{N-1} \equiv 1 \pmod{N} $$

$$ (ab)^{N-1} \not\equiv 1 \pmod{n} $$

对每个骗子b,我们都至少有一个 ab mod N是老实的,所以老实的至少和骗子的一样多。所以至少有一半a满足。

我们现在忽略Carmichael number,那么我们连挑k个小于N的a,用上面的伪代码检验。
如果我们没找到a使得 $a^{N-1} \not\equiv 1 \pmod{N}$,
那么如果N是质数,我们判断是百分之百对的。
如果N是合数,我们判断失误的概率是$\leq\frac{1}{2^k}$(因为每次挑到骗子的概率是小于 $\frac{1}{2}$ 的。

我们想用这一套算法来造随机质数
接下来我们引入一个Lagrange’s prime number theorem
$\pi(x)$ 表示不大于x的素数个数,那么有:

$$ \pi(x) \approx \frac{x}{\ln x} $$

更精确的说:

$$ \lim_{ x \to \infty } \frac{\pi(x)}{\frac{x}{\ln x}} =1 $$

然后再根据以上的素性检验,我们有一套造随机质数的算法:

  1. 随机选一个n比特数
  2. 跑素性检验
  3. 过了就输出,没过就重复

那么这个算法跑的快不快呢,我们来分析一下:
每次选一个随机的n比特数,选中质数的概率是:

$$ \frac{1}{\ln(2^n)} = \frac{1}{n\ln{2}} $$

所以大概选个$O(n)$轮就差不多了。

密码学:
考虑场景:我想把一串字符发给另一个人,但是我发送过去的消息可能被截取,所以我们想要对字符进行加密。

Private-key schemes: one-time pad
我们两个私下见了一次面,确定了一串和发送消息等长的字符,这样我发送的时候用这串字符异或一次,消息到对面了他再用这串字符异或一次,就得到了原文。

这样非常的安全,对面穷举也很难破解。但是我们的开销也很大:我们需要预共享海量的加密用字符,成本极高,所以我们需要引入公钥.

Public-key Cryptography:
我的目标就是:我留一串公钥在网上,谁都能用这个公钥加密,但是只有我能用我的私钥解密。
一个经典的加密方法:RSA(Rivest-Shamir-Adelman)
我们挑两个质数p, q,令 N = pq,我们就可以取任意一个与(p-1)(q-1)互质的e作为公钥,再在取e在模(p-1)(q-1)意义下的乘法逆元作为私钥。
我们首先区分:网络上大家都能看到e和N,但是不知道d。这个加密成立的前提是我们认为质因数分解是困难的,也就是我们很难快速分解找到p和q进而得到d。
加密:我把要发送的信息 $x$ 变成 $x^e$ mod N,发送给对面
解密:对面用 $(x^e)^d$ mod N 计算就得到 $x$
说明一下正确性:
首先我们有:

$$ ed \equiv 1 \pmod{(p-1)(q-1)} $$

所以 $ed$ 可以写成:

$$ ed = k(p-1)(q-1) +1 $$

所以有:

$$ x^{ed} - x = x(x^{k(p-1)(q-1)}-1) $$

由于 $p$ 是质数,所以

$$ \begin{align} x^{p-1} \equiv 1 \pmod{p} \\ x^{k(p-1)(q-1)} \equiv 1 \pmod{p} \end{align} $$

所以

$$ p |x^{ed} -x $$

同理,对q也成立。所以 $N |\text{(}x^{ed} - x$ mod N)。进而得到

$$ x^{ed} \text{ mod N} = x $$

全域哈希:
我们有250个客户,每个客户有一个ip地址,我想为每个ip地址标注上客户的名字,但是ip地址有$2^{32}$ 种可能性。我们自然也可以写一个250个if else,但是这样每次寻找名字的复杂度都是 $O(n)$。
一个想法是:我们寻找一个哈希函数,把每个ip地址映射到一个250个元素的表里。这样我们每次拿到ip地址做一次映射就能找到对应的名字了。
但是哈希函数常有冲突,所以实际上我们实现的时候每个元素其实是个桶,每次进入一个ip地址我们扔进这个桶里,查找时先应用哈希找桶再在桶里找,可以显著降低时间消耗。
但是,没有一个哈希函数是适用于所有情况的,我们总能找到一组奇异的数据来使得我们的哈希退化到 $O(n)$ 。所以我们的解决方案是:我们从某类函数中随机选一个函数作为我们的哈希函数。
比如有一个ip地址 $x = (x_{1},x_{2},x_{3},x_{4})$,我们随机挑一组数$(a_{1},a_{2},a_{3},a_{4}) \in [0,n-1]$,计算哈希函数为:

$$ h(x) = (a_{1}x_{1}+a_{2}x_{2}+a_{3}x_{3}+a_{4}x_{4})\text{ mod 257} $$

这样两个ip地址哈希冲突的概率就是 $\frac{1}{n}$。

Chapter 2. Divide-and-Conquer Algorithms

分治算法的主体思路:

  1. 把问题分成几个子问题
  2. 解决子问题
  3. 合并子问题

乘法:
我们知道乘法运算的时间复杂度是比加法高一个量级的。所以我们下面讨论复杂度的时候仅讨论乘法即可。
我们想要计算 $(a+bi)(c+di) = ac-bd+(bc+ad)i$,那么要经过4次乘法运算。
但是我们发现:$bc+ad = (a+b)(c+d)-ac-bd$,这样我们只用进行3次乘法运算即可。
我们接下来将阐述如何用这个思路简化整数的乘法运算。
考虑整数 $x \times y$
我们认为$x,y$是两个n比特数,其中n是2的次方。(不够的话补0就行了)
我们把$x,y$换成2进制表示。分别截取前 $\frac{n}{2}$ 位和后 $\frac{n}{2}$ 位得到 $x_{L}, x_{R},y_{L},y_{R}$,由二进制定义易知

$$ \begin{align} x = 2^{ \frac{n}{2} }x_{L} +x_{R} \\ y = 2^{ \frac{n}{2} }y_{L} +y_{R} \end{align} $$

于是乎,我们就有

$$ xy=2^nx_{L}y_{L}+2^{\frac{n}{2}}(x_{L}y_{R}+x_{R}y_{L})+x_{R}y_{R} $$

有加法和乘以2的次方都是线性的(直接在后面加0就行了)
我们记 $T(n)$ 为n位输入下要跑的时间,那对于乘法,就能写出:

$$ T(n)=4T\left( \frac{n}{2} \right) + O(n) $$

运用后面会学的主定理我们可以解出 $T(n) = O(n^2)$

那我们在这里运用一下上面提到的4次乘法变3次的技巧:
给出伪代码:

MULTIPLY($x$, $y$)
Require: Positive integers $x$ and $y$ in binary
Ensure: Their product $x \cdot y$
$n = \max(\text{size of } x, \text{size of } y)$ rounded up to the next power of $2$
if $n = 1$ then
return $x \cdot y$
end if
$x_L, x_R = \text{leftmost } n/2, \text{ rightmost } n/2$ bits of $x$
$y_L, y_R = \text{leftmost } n/2, \text{ rightmost } n/2$ bits of $y$
$P_1 = \text{MULTIPLY}(x_L, y_L)$
$P_2 = \text{MULTIPLY}(x_R, y_R)$
$P_3 = \text{MULTIPLY}(x_L + x_R,\; y_L + y_R)$
return $P_1 \times 2^n + (P_3 - P_1 - P_2) \times 2^{n/2} + P_2$

此时有递推关系:

$$ T(n)=3T\left( \frac{n}{2} \right) + O(n) $$

可以解出 $T(n) = O(n ^{\log_{2}3})$

递推关系:

Master theorem(主定理)
如果有:

$$ T(n) = aT\left( \left\lceil \frac{n}{b} \right\rceil \right)+O(n^d) $$

那么我们将得到:

$$ T(n) = \begin{cases} O(n^d) & \text{if } d > \log_b a \\ O(n^d \log n) & \text{if } d = \log_b a \\ O(n^{\log_b a}) & \text{if } d < \log_b a. \end{cases} $$

简要证明:
我们先考虑这个式子的含义:我们把一个问题分成了b个子问题,在合并子问题的时候做了a次操作。所以这棵树的高度为 $\log_{b}n$。
我们现在考虑第k层所用的时间:
合并子问题的操作数为 $a^k$,每个子问题的规模是 $\frac{n}{b^k}$,每个子问题的代价是$O\left( \left( \frac{n}{b^k} \right)^d \right)$
故每一层的代价是

$$ a^k \cdot O\left( \left( \frac{n}{b^k} \right)^d \right) = \left( \frac{a}{b^d} \right)^k \cdot O(n ^d) $$

总时间就是求和:

$$ \sum_{k=0}^{\log_{b}n}O(n^d) \cdot\left( \frac{a}{b^d} \right)^k $$

这是个等比数列求和,再对3种大小关系讨论一下就得到主定理了。

归并排序:
我相信你的程序设计课肯定学了这个,这里就不赘述了。
值得注意的是我们在这里讲到了强比较算法的时间下界。
我们可以把一个排序写成一棵决策树,每个叶子节点表示一个排列,每个内部节点表示一次比较,每条边表示这个节点是否为真从而导入下一个节点。对于一个序列,排好序的状态有 $n!$ 种,所以这棵二叉决策树的高为 $\log(n!) = O(n\log n)$,所以基于比较的排序有 $O(n\log n)$ 的下界。
而部分不依赖比较的排序方法,比如基数排序和桶排序,在某些数据下可以打破这个下界。作业中会看到。

中位数:
一个朴素的想法:直接排序找中间就行。预期复杂度是 $(n\log n)$ 。但是我们只想要中位数,不需要两两之间的大小关系,所以我们有希望把这个算法降成线性的。
我们采用以下算法:
我们随机选取v。
对于每个v,把这组数分成3组:小于v的,等于v的和大于v的。判断v是否是中位数即可。
理想情况下,小于v的和大于v的都大概在 $\frac{n}{2}$ 左右,我们能写出以下递推公式:

$$ T(n) = T\left( \frac{n}{2} \right) + O(n) $$

故而预期时间复杂度是线性的。
然而呢,最坏情况下,我每次都选到边界,导致算法会退化到 $O(n^2)$。
那么现在我们这个算法的平均预期复杂度是多少呢?还是线性的。
给出一个简短的证明:
我们认为选取到的v在25百分位数和75百分位数之间是好的选取。
一次好的选取的概率是 $\frac{1}{2}$,故预期每两次就能得到一次好的选取。
故有:

$$ T(n)\leq T\left( \frac{3}{4}n \right) +O(n) $$

从而得到是线性的。

矩阵乘法:
显然一个朴素的矩阵乘法是 $O(n^3)$ 的。
对它使用分治吧!

$$ X= \begin{bmatrix} A & B \\ C& D \end{bmatrix} ,Y= \begin{bmatrix} E&F \\ G&H \end{bmatrix} $$ $$ XY = \begin{bmatrix} AE+BG&AF+BH \\ CE+DG&CF+DH \end{bmatrix} $$

每次需要计算AE,BG,AF,BH,CE,DG,CF,DH,并做一次 $O(n^2)$ 的加和。
有递推公式:

$$T(n) = 8T\left( \frac{n}{2} \right)+O(n^2)$$

算出来仍是 $O(n^3)$ 的
然而,有人惊人的注意到,倘若我们这样算,居然只用作7次子矩阵乘法

$$ XY = \begin{bmatrix} P_5 + P_4 - P_2 + P_6 & P_1 + P_2 \\ P_3 + P_4 & P_1 + P_5 = P_3 - P_7 \end{bmatrix} $$

其中

$$ \begin{aligned} P_1 &= A(F - H) \\ P_2 &= (A + B)H \\ P_3 &= (C + D)E \\ P_4 &= D(G - E)\\ P_5 &= (A + D)(E + H) \\ P_6 &= (B - D)(G + H) \\ P_7 &= (A - C)(E + F) \end{aligned} $$

这样我们就有递推公式:

$$ T(n)=7T\left( \frac{n}{2} \right)+O(n^2) $$

进而由主定理得到 $T(n)=O(n^{\log_{2}7})$

快速傅里叶变换(FFT):
FFT是加速卷积(多项式乘法)的一个算法。(是翌佳最爱的算法之一)
我们考虑现在有两个序列:

$$ A(x)=a_{0}+a_{1}x+\dots+a_{d}x^d, B(x)=b_{0}+b_{1}x+\dots+b_{d}x^d $$ $$ C(x)=A(x) \cdot B(x) = c_{0} + c_{1}x+\dots+c_{2d}x^{2d} $$

其中

$$c_{k}=\sum_{i=0}^{k}a_{i}b_{{k-i}}$$

算这个的时间复杂度是 $\Theta(d^2)$的。
我们注意到,一个d次多项式的表示方法其实有两种。我们除了可以写出d+1个系数外,还可以由这个多项式的d+1个点来确定。(类似于两点确定一条直线)。
那么我想要确定 $C(x)$ 的所有系数,我需要找到 2d+1 个点在 C 上的取值。
因此,我们引入了fft与ifft,快速求值与快速插值。
简单来讲,我们通过fft快速求出A,B分别在 2d+1 个点上的取值,然后我们把这些点逐点相乘就得到了 C 在 2d+1 个点上的取值。最后我们通过ifft用 C 在这 2d+1 个点上取值的快速还原 C 的各个系数。这就是大致思想,我们接下来介绍如何快速求值和快速插值。
我们接下来给出一般性的fft,所以我们不妨假定n是2的幂次(不够补到就行了)
我们选择n个点:

$$ \pm x_{0},\pm x_{1} \dots \pm x_{\frac{n}{2}-1} $$

我们先把 $A(x) = a_{0} + a_{1}x + \dots + a_{n}x^n$拆一下,得到:

$$ \begin{align} A(x)=&a_{0}+a_{2}x^2+\dots+a_{n}x^n +(a_{1}x+a_{3}x^3+\dots+a_{n-1}x^{n-1}) \\ =&a_{0}+a_{2}x^2+\dots+a_{n}x^n +x(a_{1}+a_{3}x^2+\dots+a_{n-1}x^{n-2}) \\ =&A_{e}(x^2)+xA_{o}(x^2) \end{align} $$

这样拆有什么好处呢?注意到我们选择的这n个点正负两两配对,所以有

$$ \begin{align} A(x)= A_{e}(x^2)+xA_{o}(x^2) \\ A(-x)= A_{e}(x^2)-xA_{o}(x^2) \end{align} $$

也就是说我们只要算出 $A_{e}(x^2)$ 和 $A_{o}(x^2)$ 就能得到这两个点。我们就成功的把这个问题分治成了两个子问题。那我们随便挑一个来看,看到 $A_{e}(x^2)$,我们拥有的点是:

$$ x_{0}^2, x_{1}^2\dots ,x_{\frac{n}{2}-1}^2 $$

那我还是想让这 $\frac{n}{2}$ 个点两两正负配对从而实现分治,为了使平方数仍然有正负配对,所以我们引入虚数作为点的选择。
选取

$$ z^n = 1 $$

的n个单位根: $1,\omega,\omega^2\dots \omega^{n-1}$,其中 $\omega = e^{\frac{2\pi i}{n}}$
由单位根性质,$\omega^{j+\frac{n}{2}} =-\omega^j$,每次就取正的$\omega^{1\to \frac{n}{2}-1}$平方,原来差 $\frac{\pi}{4}$ 的平方之后又差 $\frac{\pi}{2}$ ,又形成了正负配对。进而我们成功把这个问题不断递归下去,直到只有一个点(经过n次平方,取到1)的时候,我们就直接计算即可。
给出伪代码:

Fast Fourier Transform (FFT)
Require: Coefficient representation of polynomial $A(x)$ of degree $\le n-1$, where $n$ is a power of $2$; an $n$th root of unity $\omega$.
Ensure: Value representation $A(\omega^0), A(\omega^1), \ldots, A(\omega^{n-1})$.
if $\omega = 1$ then
return $A(1)$
else
Express $A(x) = A_e(x^2) + x A_o(x^2)$
$(s_0, \ldots, s_{n/2-1}) = \text{FFT}(A_e, \omega^2)$
$(t_0, \ldots, t_{n/2-1}) = \text{FFT}(A_o, \omega^2)$
for $j = 0$ to $n/2-1$ do
$r_j = s_j + \omega^j t_j$
$r_{j+n/2} = s_j - \omega^j t_j$
end for
return $(r_0, r_1, \ldots, r_{n-1})$
end if

不难发现:

$$ T(n)=2T\left( \frac{n}{2} \right)+O(n) $$

故得到fft的时间复杂度为 $O(n\log n)$

接下来考虑如何通过ifft把取值还原成系数。
我们通过fft算出A在n个单位根的取值了对吧,那我们可以写成:

$$ \begin{bmatrix} A(1) \\ A(\omega)\\ \vdots \\ A(\omega^{n-1}) \end{bmatrix} = \begin{bmatrix} 1 &1 &1&\dots &1 \\ 1 &\omega &\omega^{2} &\dots &\omega^{n-1} \\ & & \vdots \\ 1 &\omega^j &\omega^{2j} &\dots &\omega^{(n-1)j} \\ & & \vdots \\ \\ 1 &\omega^{n-1} & \omega^{2(n-1)} &\dots &\omega^{(n-1)(n-1)} \end{bmatrix} \begin{bmatrix} a_{0} \\ a_{1} \\ \vdots \\ a_{n-1} \end{bmatrix} $$

$$ M_{n}(\omega)= \begin{bmatrix} 1 &1 &1&\dots &1 \\ 1 &\omega &\omega^{2} &\dots &\omega^{n-1} \\ & & \vdots \\ 1 &\omega^j &\omega^{2j} &\dots &\omega^{(n-1)j} \\ & & \vdots \\ \\ 1 &\omega^{n-1} & \omega^{2(n-1)} &\dots &\omega^{(n-1)(n-1)} \end{bmatrix} $$

则有:(挺显然的)

$$ M_{n}^{-1}(w)=\frac{1}{n}M_{n}(\omega^{-1}) $$

那么我们把fft得到的结果作为输入,再以 $\omega^{-1}$为单位根做一遍fft最后除以n就得到了原来的系数。

逆序对:
程序设计课肯定讲了,不再赘述。

Chapter 3. Decompositions of graphs

两种存储方式:邻接表与邻接矩阵

DFS: 程序设计课一定学过。
我们引入一些dfs的概念:
我们定义一个节点的pre:第一次走到这个点的时间
post:完成这个点的时间(即最后一次来到这个点)
首先是无向图中的:

  1. 树边:dfs树上的边
  2. 回边:剩下的边

有向图中:

  1. 树边:dfs树上的边
  2. 前向边:一个节点指向非子节点的后代的边
  3. 回边:一个节点指向祖先的边
  4. 交叉边:指向一个已完成的点(即两个子树间相连的边)

我们考虑用一条边两个节点的(pre,post) (u->v)来判断这条边的类型
$[_{u} [_{v} \quad ]_{v} ]_{u}$ :树边\前向边
$[_{v} [_{u} \quad ]_{u} ]_{v}$ :回边
$[_{u} ]_{u} \quad [_{v}]_{v}$ :交叉边
如果找到一条回边那么这个图就有环。

拓扑排序(Linearization/Topologically Sort):
这是针对DAG的算法。我们的目的是使得每条边都从序数较小的点去到序数较大的点。
我们定义两个概念:sink:无出边;source:无入边。
我们每次找到一个source,记录并删去它,重复直到图为空。

强连通分量(Strongly connected components\SCC):
定义强连通分量:在该子图里任意两点u,v均存在u->v的路径与v->u的路径
任何有向图都能转换成其强连通分量的DAG(如果有环,那环上的就不是强连通分量,可以把整个环合成强连通分量)

如果我们从一个点u出发开始找(dfs就行),那么它应该在把u可达的所有节点跑完后终止。
如果一个节点在sink scc中,那么从这个节点开始跑,得到的最终结果就应该是sink scc。(从定义出发不难理解)
那么我们现在就有两个问题:
(a):我们如何找到位于sink scc里的节点
(b):找到了sink scc又如何找到下一个。

为解决这些问题,我们引入两个Lemma:

  1. 如果 强连通分量C里有一个节点 有一条指向 强连通分量 C'里的节点(可以看做meta graph上C有一条边指向C'),那么C中最大的post值一定比C'里大。(可以考虑dfs的顺序,由于C指向C‘,则C一定在C’后完成,post值一定更大)。
  2. post最大的点一定在source scc里(由1可知)

也就是说,我们一定知道source scc里的一个点,这与我们的问题(a)有点出入,于是我们考虑反图。
即我们把图G中所有的边反向得到图 $G^R$,首先,反图的强连通分量一定与原图相同,且原图的source scc就变成sink scc了。于是我们自然而然的就得到了以下线性算法:
在反图上跑一遍dfs得到post值,然后在原图上挑post值最大的没有遇到过的点为起点跑一次dfs。这样每次跑一遍dfs得到的点就是一个强连通分量。

Chapter 4. Paths in Graphs

bfs程序设计课上肯定也讲了。
Dijkstra’s algorithm:
我们知道,bfs可以找到边权为1的图中的最短路,如果边权不为1呢?
一个简单的想法:把边权不为1的边裁成若干个边权为1的边然后建虚点。很明显边权比较大的时候这是非常不妙的。
(待续。。。)

为什么dijkstra不能在负权图上使用呢?我们考虑下面这个图:
1->2 3
1->3 5
3->2 -3
那么以1为起点,用dijkstra可以到2的最短路是1->2是3,但是很明显,1->3->2为2更短。其原因在于:存在负边,则当前最小不一定是全局最小。
那对于含有负权边的图该怎么办呢,我们引入如下算法:
Bellman-Ford algorithm:
我们先引入一个松弛操作:
UPDATE(u,v):dist(v)=min(dist(v), dist(u)+l(u,v))
如果dist(u)是到u的最短路,且u是到v的最短路上的倒数第二个点,那么v的最短路也算出来了。
那么怎么才能保证以上这两点呢?我们松弛|V|-1次。
给出伪代码:

SHORTEST-PATHS($G$, $\ell$, $s$)
Require: Graph $G = (V, E)$; edge lengths $\{\ell_e \mid e \in E\}$ with no negative cycles; vertex $s \in V$
Ensure: For all vertices $u$ reachable from $s$, $\text{dist}(u)$ is set to the distance from $s$ to $u$.
for all $u \in V$ do
$\text{dist}(u) = \infty$
$\text{prev}(u) = \text{nil}$
end for
$\text{dist}(s) = 0$ \\
Repeat $|V|-1$ times
for all $e \in E$ do
$\text{update}(e)$
end for

对第i次循环,dist(u)表示的是:从起点出发,最短路径不超过i条边的路径长度。因为每一轮松弛,我们只能保证最短路径小于i条边时的答案是对的。如果没有负环,那么最短路径最多经过|V|-1条边,所以循环|V|-1次就行了。
那么如果循环|V|次了,还有dist(u)在更新,这就说明,到这个点的最短路有|V|条边,那么一定存在负环。故这个算法也能用来检测负环。

DAG中的最短路:
可以直接类似动态规划,由于DAG中无环,所以我们按照拓扑排序的顺序一步步更新值即可。
给出伪代码:

DAG-SHORTEST-PATHS($G$, $\ell$, $s$)
Require: DAG $G = (V, E)$; edge lengths $\{\ell_e \mid e \in E\}$; vertex $s \in V$
Ensure: For all vertices $u$ reachable from $s$, $\text{dist}(u)$ is set to the distance from $s$ to $u$.
for all $u \in V$ do
$\text{dist}(u) = \infty$
$\text{prev}(u) = \text{nil}$
end for
$\text{dist}(s) = 0$
Linearize $G$
for each $u \in V$ in linearized order do
for all edges $(u, v) \in E$ do
$\text{update}(e)$
end for
end for

在这个条件下最长路也可求:对每条边取负,求完最短路再取负即可。

Chapter 5. Greedy algorithms

Minimum spanning trees:
简单来讲,就是在一个图中,选出一棵覆盖所有节点的树,并使得其边权和最小。
我们引入Kruskal算法:简单来讲就是先把边排好序,挑出最小的边,如果这条边加入当前的树中不构成环,就加入,否则就下一条,直到加入 |V|-1条为止。
如何保证正确性呢?我们引入一个比较重要的Lemma: Cut Property
考虑X是MST的一部分,把图划分成包含X和不包含X的两部分(S与V\S)。那么取连接S与V\S两部分的最小的一条边e,$X \cap \{e\}$ 也是MST的一部分。还挺显然吧(),证明见讲义。大概思路就是如果$X \cap \{e\}$ 是MST一部分,那就不用证明;如果不是,我们就构造出一棵新树。具体操作就是已知是一棵连通的树,我们加入e,那就必须删除一条连接S与V\S两部分的那条边,不然就会有环,删除后也不影响连通性。此时边权和小于等于原来的边权和,故其一定是MST。

给出伪代码:

KRUSKAL($G$, $w$)
Require: A connected undirected graph $G = (V, E)$ with edge weights $w_e$
Ensure: A minimum spanning tree defined by the edge set $X$
for all $u \in V$ do
$\text{makeset}(u)$
end for
$X \gets \emptyset$
Sort the edges $E$ by increasing weight
for all edge $\{u, v\} \in E$ in increasing order of weight do
if $\text{find}(u) \neq \text{find}(v)$ then
Add edge $\{u, v\}$ to $X$
$\text{union}(u, v)$
end if
end for

新的问题来了,我们的union和find应该怎么写才能保证能成功判断环呢?
我们采取按秩合并。(并查集)

MAKESET
function MAKESET($x$)
$\pi(x) \gets x$
$\text{rank}(x) \gets 0$
end function
FIND
function FIND($x$)
while $x \neq \pi(x)$ do
$x \gets \pi(x)$
end while
return $x$
end function
UNION
function UNION($x, y$)
$r_x \gets \text{FIND}(x)$
$r_y \gets \text{FIND}(y)$
if $r_x = r_y$ then
return
end if
if $\text{rank}(r_x) > \text{rank}(r_y)$ then
$\pi(r_y) \gets r_x$
else
$\pi(r_x) \gets r_y$
if $\text{rank}(r_x) = \text{rank}(r_y)$ then
$\text{rank}(r_y) \gets \text{rank}(r_y) + 1$
end if
end if
end function

可见我们最后大致把find转换成了在树形结构上寻找。此时每次操作的复杂度都是 $O(\log n)$。
有3个性质:

  1. 如果x不是根,$rank(\pi(x))>rank(x)$(由定义可得)
  2. 任何秩为k的点,子树大小至少为 $2^k$(归纳法易证)
  3. 最多只有 $\frac{n}{2^k}$ 个的点的秩到达过k(作业会证)

因此,目前来讲,时间的瓶颈主要还是在
O(|E| log |V |) 来排序
O(|E| log |V |) 来union和find。
假使我们把边排好序了在输入,能不能优化呢,也就是说我们能不能优化我们并查集的复杂度。
我们引入路径压缩:

FIND (with path compression)
function FIND($x$)
if $x \neq \pi(x)$ then
$\pi(x) \gets \text{FIND}(\pi(x))$
end if
return $\pi(x)$
end function

应用路径压缩,我们可以把原 $O(\log n)$ 的复杂度优化至接近常数。
简单来讲,也就是我们find时每次都把路上碰见的节点挂靠的根上,这样之后每次对它查找都快得多。
以下是对路径压缩后的并查集复杂度的证明:
我们把n划分到以下区间:

$$ [1],[2],[3,4],[5,6,\dots,16]\dots[k+1,\dots,2^k] $$

区间个数为 $\log^ n$,定义为n连续取log最后小于1的次数。
一旦一个点不是根了,其秩就固定了,我们为其发放$2^k$的零花钱(秩落在$[k+1,\dots,2^k]$里)
总零花钱:对每个区间 $I_k$,秩 > $k$ 的节点数 ≤ $n/2^k$,因此该区间内所有节点获得的总钱数 ≤ n。
共有 $\log^
n$ 个区间,所以总零花钱 ≤ $n \log^* n$。

一次 find(x) 从 x 向上走到根,沿途节点 $v_0=x, v_1, \dots, v_t = \text{root}$。
将每一步 $(v_i \to v_{i+1})$ 分为两类:

这样的步数最多等于区间个数,即 $O(\log^* n)$。

这种步的代价由节点 $v_i$ 用自己的零花钱支付(1 元/步)。

设节点 v 的秩落在区间 $I_k = \{k+1,\dots,2^k\}$,它获得 $2^k$ 元。
每次 v 支付一元时,它的父节点秩严格增加(因为路径压缩会将 v 指向一个更高秩的节点,或最终指向根)。
由于父节点秩 ≤ $2^k$,且每次至少 +1,故 v 最多支付 $2^k$ 次就会使父节点秩超出该区间。
此后 v 再也不会遇到同区间爬升。因此 v 的零花钱恰好够支付所有同区间步。

设共有 m次 find 操作。

因此总时间 $T(m,n) = O(m \log^ n + n \log^ n) = O((m+n)\log^ n)$。
平摊到每次 find 为 $O(\log^
n)$。

Set Cover:
找到最小的点集,使得整个图中的所有点要么在这个点集中,要么与这个点集中的点相连
一个贪心的想法:每次加入与最多的未覆盖的点相连的点。直到彻底覆盖。
但是这个并不总是成立的。给出一个最简单的反例:
1-2-3-4-5
我如果第一次挑到3,那么就需要挑3个点,而很明显挑2和4两个点就够了。贪心算法失效了。
但是贪心算法虽然失效了,但是贪心可以保证我们的答案离最优解差的并没有很远。

假设 B 包含 n 个元素,且最优覆盖由 k 个集合组成。那么贪心算法至多使用 $k \ln n$ 个集合。
证明:设 $n_t$ 为贪心算法经过 $t$ 次迭代后仍未覆盖的元素个数(因此 $n_0 = n$)。由于剩余元素可以被最优的 $k$ 个集合覆盖,必然存在某个集合至少包含其中的 $n_t / k$ 个元素。因此,贪心策略将保证

$$ n_{t+1} \leq n_t - \frac{n_t}{k} = n_t \left( 1 - \frac{1}{k} \right) $$

重复应用该不等式可得

$$ n_t \leq n_0 \left( 1 - \frac{1}{k} \right)^t $$

Chapter 6. Dynamic programming

前文已经讲到了DAG中的最短路就有dp的思想。
dp的最朴素思想就是解决某些子问题,然后我们可以通过子问题快速得到目前的答案。

Longest increasing subsequences:
先有一个转化:
我们对整个序列建图:如果 $a_{i} < a_{j}$ 且 $i < j$,则在i,j建一条权为1的边。
最后在这个DAG里找最长路就行。
$L_{j}=1+max\{L_{i},(i,j) \in E\}$
从这里来看,dp本质就是从某个子问题转移到另一个子问题,比如在这里,我们就解决了以 $a_{i}$ 结尾的最长上升子序列,于是我们就可以从这个子问题转移到以 $a_{j}$ 结尾的最长上升子序列中。
给出伪代码:

for $j=1$ \To $n$ do
$L_{j}=1+max\{L_{i},(i,j) \in E\}$
end for
return $max_j\{L_j\}$

Edit distance:
比如我想在输入法里输入你好,即打入“nihao”,但是一手滑输入成了“nihoa”。现在大部分智能的输入法都能自动识别出你想要的是“nihao”。识别的重要依据就是两个字符串间的 edit distance,即把一个字符串修改为另一个字符串的最少修改次数。一个例子是:
从 sanow ->snowy,如果我们逐字替换,要用4次,但是很明显我们可以对比:
sanow_
s_nowy
这样改两次就行了。
可以抽象成以下两个集合间的距离:

$$ x[1,\dots,m] \text{ and } y[1,\dots,n] $$

定义E(i, j)为 $x[1,\dots,i]$ 和 $y[1,\dots,j]$ 的最少修改次数。
那么我们得到状态转移方程:

$$ E(i,j)=min\{1+E(i-1,j),1+E(i,j-1),diff(i,j)+E(i-1,j-1)\} $$

其中

$$ diff(i,j)= \begin{cases} 1 ,x_{i} \not=y_{j}\\ 0,x_{i} = y_{j} \end{cases} $$

翻译一下:1+E(i-1,j)就是把 $x_{i}$ 删了,1+E(i,j-1)就是把 $y_{j}$ 插进来,剩下那个就是如果相同就不动,不同就把 $x_{i}$ 改成 $y_{j}$ 。最后E(m,n)就是我们要的答案。
给出伪代码:

for $i=0$ \To $m$ do
$E(i,0)=i$
end for
for $j=1$ \To $n$ do
$E(0,j)=j$
end for
for $i=1$ \To $m$ do
for $j=1$ \To $n$ do
$E(i,j)=min\{1+E(i-1,j),1+E(i,j-1),diff(i,j)+E(i-1,j-1)\}$
end for
end for
return $E(m,n)$

Knapsack:
背包问题的背景不再赘述。dp能做到 $O(n \cdot W)$。
值得注意的是,如果输入不是W量级而是$\log W$量级的话(也可以理解为W太大了),普通dp就不能做到多项式时间了。
对每个$w < W$,我们定义 $K(w)$ 为最多装w的情况下获得的最大价值。
那么现在对于无限背包问题(每个物品没有数量限制),有状态转移方程:

$$ K(w)=\max_{i:w_{i}给出伪代码:

$K(0)=0$
for $w=1$ \To W do
$K(w)=\max_{i:w_{i}
end for
return $K(W)$

那么对于01背包问题,每个物品只有选和不选,我们加入一个限制:定义 $K(w,j)$ 为只用前j个物品最多装w的情况下获得的最大价值。
有状态转移方程:

$$ K(w,j)=\max \{K(w-w_{j},j-1)+v_{j},K(w,j-1)\} $$

给出伪代码:

Initial all $K(0,j) = 0$ and $K(w,0) = 0$
for $j=1$ \To n do
for $w=1$ \To W do
if $w_j > w$ then
$K(w,j)=K(w,j-1)$
end if
$K(w,j)=\max \{K(w-w_{j},j-1)+v_{j},K(w,j-1)\}$
end for
end for
return $K(W,n)$

Chain matrix multiplication:
也是经典dp,不赘述。仅给出伪代码:

for $i=1$ \To $n$ do
C(i,i)=0
end for
for $s = 1$ \To $n-1$ do
for $i=1$ \To $n-s$ do
$j = i + s$
$C(i,j)=\min_{i \leq k < j}{C(i,k)+C(k+1,j)+m_{i-1}*m_{k}*m_{j}}$
end for
end for

The traveling salesman problem:
一位旅行商正准备进行一次大型销售之旅。他从家乡出发,在旅程中,每个目标城市恰好访问一次,最后返回家乡。已知城市之间的两两距离,如何确定最佳的访问顺序,以使总旅行距离最小?

将城市编号为 $1, \ldots, n$,旅行商的家乡为 1,并设
$D = (d_{ij})$ 为城市间的距离矩阵。目标是设计一条从 1 出发并结束于 1 的路线,包含所有其他城市恰好一次,且总长度最短。

暴力方法是对每一条可能的路线进行评估,并返回最佳结果。由于共有 $(n-1)!$ 种可能性,该策略的时间复杂度为 $O(n!)$。

对于包含城市 $S \subseteq \{1, 2, \dots, n\}$ 且 $1 \in S$ 的子集,以及 $j \in S$,定义 $C(S, j)$ 为从 1 出发,恰好访问 $S$ 中每个城市一次,并结束于 $j$ 的最短路径长度。

当 $|S| > 1$ 时,我们定义 $C(S, 1) = \infty$。

对于 $j \neq 1$ 且 $j \in S$,有

$$ C(S, j) = \min_{i \in S: i \neq j} C(S \setminus \{j\}, i) + d_{ij}. $$

我们给出伪代码:

$C(\{1\},1)=0$
for $s=2$ \To $n$ do
for all subsets $S \subset \{1,\dots,n\}$ of size $s$ and containing 1 do
$C(S,1) = \infty$
for all $j \in S$ and $j \not= 1$ do
$C(S, j) = \min_{i \in S: i \neq j} C(S \setminus \{j\}, i) + d_{ij}$
end for
end for
end for
return $\min_{j}C(\{1,\dots,n\},j)+d_{j1}$

至多有$2^n \cdot n$ 个子问题(有$O(2^n)$个 $S$,每个S找 $O(n)$ 个 $j$),每个子问题线性,所以总时间为 $O(n^2 \cdot 2^n)$

Independent sets in trees:
先摆一句yjgg的话:“我希望你们培养一种直觉,就是树上的状态转移一般都是从叶子到根。”
图 $G = (V, E)$ 的一个节点子集 $S \subseteq V$ 称为独立集,如果其中任意两个节点之间都没有边相连。
寻找图中的最大独立集是NP完全的。然而,当图恰好是一棵树时,该问题可以使用动态规划在线性时间内解决。
我们定义 $I(u)$ 为 u 下辖子树内的最大独立集。那么很明显,有状态转移方程:

$$ I(u) = \max \left\{1+ \sum_{\text{grandchildren w of u}}I(w),\sum_{\text{children w of u}}I(w)\right\} $$

Chapter 7. Linear programming and reductions

我现在有一堆变元,和一堆关系(等式或不等式),然后我想要最大化一个目标函数。